package com.alecgorge.minecraft.jsonapi.packets.netty.router; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.QueryStringDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.code.regexp.NamedMatcher; import com.google.code.regexp.NamedPattern; /** * This class allows you to do route requests based on the HTTP verb and the * request URI, in a manner similar to <a * href="http://www.sinatrarb.com/">Sinatra</a> or <a * href="http://expressjs.com/">Express</a>. * <p> * RouteMatcher also lets you extract parameters from the request URI either a * simple pattern or using regular expressions for more complex matches. Any * parameters extracted will be added to the requests parameters which will be * available to you in your request handler. * <p> * It's particularly useful when writing REST-ful web applications. * <p> * To use a simple pattern to extract parameters simply prefix the parameter * name in the pattern with a ':' (colon). * <p> * Different handlers can be specified for each of the HTTP verbs, GET, POST, * PUT, DELETE etc. * <p> * For more complex matches regular expressions can be used in the pattern. When * regular expressions are used, the extracted parameters do not have a name, so * they are put into the HTTP request with names of param0, param1, param2 etc. * <p> * Multiple matches can be specified for each HTTP verb. In the case there are * more than one matching patterns for a particular request, the first matching * one will be used. * <p> * Instances of this class are not thread-safe * <p> * * @author <a href="http://tfox.org">Tim Fox</a> * @author <a href="http://alecgorge.com">Alec Gorge</a> */ public class RouteMatcher extends SimpleChannelInboundHandler<FullHttpRequest> { private final List<PatternBinding> getBindings = new ArrayList<PatternBinding>(); private final List<PatternBinding> putBindings = new ArrayList<PatternBinding>(); private final List<PatternBinding> postBindings = new ArrayList<PatternBinding>(); private final List<PatternBinding> deleteBindings = new ArrayList<PatternBinding>(); private final List<PatternBinding> optionsBindings = new ArrayList<PatternBinding>(); private final List<PatternBinding> headBindings = new ArrayList<PatternBinding>(); private final List<PatternBinding> traceBindings = new ArrayList<PatternBinding>(); private final List<PatternBinding> connectBindings = new ArrayList<PatternBinding>(); private final List<PatternBinding> patchBindings = new ArrayList<PatternBinding>(); private Handler<FullHttpResponse, RoutedHttpRequest> noMatchHandler; private Handler<Void, RoutedHttpResponse> everyMatchHandler; @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { serveRequest(ctx, request); } List<PatternBinding> getBindingsForRequest(FullHttpRequest request) { HttpMethod m = request.getMethod(); if (m.equals(HttpMethod.GET)) { return getBindings; } else if (m.equals(HttpMethod.PUT)) { return putBindings; } else if (m.equals(HttpMethod.POST)) { return postBindings; } else if (m.equals(HttpMethod.DELETE)) { return deleteBindings; } else if (m.equals(HttpMethod.OPTIONS)) { return optionsBindings; } else if (m.equals(HttpMethod.HEAD)) { return headBindings; } else if (m.equals(HttpMethod.TRACE)) { return traceBindings; } else if (m.equals(HttpMethod.PATCH)) { return patchBindings; } else if (m.equals(HttpMethod.CONNECT)) { return connectBindings; } return null; } public boolean serveRequest(ChannelHandlerContext ctx, FullHttpRequest request) { // Handle a bad request. if (!request.getDecoderResult().isSuccess()) { sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return false; } return route(ctx, request, getBindingsForRequest(request)); } /** * Specify a handler that will be called for a matching HTTP GET * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher get(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, getBindings); return this; } /** * Specify a handler that will be called for a matching HTTP PUT * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher put(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, putBindings); return this; } /** * Specify a handler that will be called for a matching HTTP POST * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher post(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, postBindings); return this; } /** * Specify a handler that will be called for a matching HTTP DELETE * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher delete(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, deleteBindings); return this; } /** * Specify a handler that will be called for a matching HTTP OPTIONS * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher options(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, optionsBindings); return this; } /** * Specify a handler that will be called for a matching HTTP HEAD * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher head(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, headBindings); return this; } /** * Specify a handler that will be called for a matching HTTP TRACE * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher trace(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, traceBindings); return this; } /** * Specify a handler that will be called for a matching HTTP CONNECT * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher connect(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, connectBindings); return this; } /** * Specify a handler that will be called for a matching HTTP PATCH * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher patch(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, patchBindings); return this; } /** * Specify a handler that will be called for all HTTP methods * * @param pattern * The simple pattern * @param handler * The handler to call */ public RouteMatcher all(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addPattern(pattern, handler, getBindings); addPattern(pattern, handler, putBindings); addPattern(pattern, handler, postBindings); addPattern(pattern, handler, deleteBindings); addPattern(pattern, handler, optionsBindings); addPattern(pattern, handler, headBindings); addPattern(pattern, handler, traceBindings); addPattern(pattern, handler, connectBindings); addPattern(pattern, handler, patchBindings); return this; } /** * Specify a handler that will be called for a matching HTTP GET * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher getWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, getBindings); return this; } /** * Specify a handler that will be called for a matching HTTP PUT * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher putWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, putBindings); return this; } /** * Specify a handler that will be called for a matching HTTP POST * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher postWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, postBindings); return this; } /** * Specify a handler that will be called for a matching HTTP DELETE * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher deleteWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, deleteBindings); return this; } /** * Specify a handler that will be called for a matching HTTP OPTIONS * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher optionsWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, optionsBindings); return this; } /** * Specify a handler that will be called for a matching HTTP HEAD * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher headWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, headBindings); return this; } /** * Specify a handler that will be called for a matching HTTP TRACE * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher traceWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, traceBindings); return this; } /** * Specify a handler that will be called for a matching HTTP CONNECT * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher connectWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, connectBindings); return this; } /** * Specify a handler that will be called for a matching HTTP PATCH * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher patchWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, patchBindings); return this; } /** * Specify a handler that will be called for all HTTP methods * * @param regex * A regular expression * @param handler * The handler to call */ public RouteMatcher allWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) { addRegEx(regex, handler, getBindings); addRegEx(regex, handler, putBindings); addRegEx(regex, handler, postBindings); addRegEx(regex, handler, deleteBindings); addRegEx(regex, handler, optionsBindings); addRegEx(regex, handler, headBindings); addRegEx(regex, handler, traceBindings); addRegEx(regex, handler, connectBindings); addRegEx(regex, handler, patchBindings); return this; } /** * Specify a handler that will be called when no other handlers match. If * this handler is not specified default behaviour is to return a 404 */ public RouteMatcher noMatch(Handler<FullHttpResponse, RoutedHttpRequest> handler) { noMatchHandler = handler; return this; } /** * Specify a handler that will be called when any other handler matchs. */ public RouteMatcher everyMatch(Handler<Void, RoutedHttpResponse> handler) { everyMatchHandler = handler; return this; } private static void addPattern(String input, Handler<FullHttpResponse, RoutedHttpRequest> handler, List<PatternBinding> bindings) { // We need to search for any :<token name> tokens in the String and // replace them with named capture groups Matcher m = Pattern.compile(":([A-Za-z][A-Za-z0-9_]*)").matcher(input); StringBuffer sb = new StringBuffer(); Set<String> groups = new HashSet<String>(); while (m.find()) { String group = m.group().substring(1); if (groups.contains(group)) { throw new IllegalArgumentException("Cannot use identifier " + group + " more than once in pattern string"); } m.appendReplacement(sb, "(?<$1>[^\\/]+)"); groups.add(group); } m.appendTail(sb); String regex = sb.toString(); PatternBinding binding = new PatternBinding(NamedPattern.compile(regex), groups, handler); bindings.add(binding); } private static void addRegEx(String input, Handler<FullHttpResponse, RoutedHttpRequest> handler, List<PatternBinding> bindings) { PatternBinding binding = new PatternBinding(NamedPattern.compile(input), null, handler); bindings.add(binding); } public FullHttpResponse getResponse(ChannelHandlerContext ctx, FullHttpRequest request) { // Handle a bad request. FullHttpResponse resp = null; if (!request.getDecoderResult().isSuccess()) { resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); sendHttpResponse(ctx, request, resp); } else { resp = getResponseForRoute(ctx, request, getBindingsForRequest(request)); } return resp; } FullHttpResponse getResponseForRoute(ChannelHandlerContext ctx, FullHttpRequest request, List<PatternBinding> bindings) { RoutedHttpRequest rreq = new RoutedHttpRequest(ctx, request); for (PatternBinding binding : bindings) { QueryStringDecoder uri = new QueryStringDecoder(request.getUri()); NamedMatcher m = binding.pattern.matcher(uri.path()); if (m.matches()) { Map<String, List<String>> params = new HashMap<String, List<String>>(m.groupCount()); if (binding.paramNames != null) { // Named params for (String param : binding.paramNames) { List<String> l = new ArrayList<String>(); l.add(m.group(param)); params.put(param, l); } } else { // Un-named params for (int i = 0; i < m.groupCount(); i++) { List<String> l = new ArrayList<String>(); l.add(m.group(i + 1)); params.put("param" + i, l); } } uri.parameters().putAll(params); FullHttpResponse res = binding.handler.handle(rreq); return res; } } if (noMatchHandler != null) { return noMatchHandler.handle(rreq); } return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); } private boolean route(ChannelHandlerContext ctx, FullHttpRequest request, List<PatternBinding> bindings) { FullHttpResponse res = getResponseForRoute(ctx, request, bindings); sendHttpResponse(ctx, request, res); if (everyMatchHandler != null) { everyMatchHandler.handle(new RoutedHttpResponse(request, res)); } return noMatchHandler != null; } void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { if (res == null) { // no http response, probably upgrading to websocket or something return; } // Send the response and close the connection if necessary. ChannelFuture f = ctx.channel().writeAndFlush(res); if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } private static class PatternBinding { final NamedPattern pattern; final Handler<FullHttpResponse, RoutedHttpRequest> handler; final Set<String> paramNames; private PatternBinding(NamedPattern pattern, Set<String> paramNames, Handler<FullHttpResponse, RoutedHttpRequest> handler) { this.pattern = pattern; this.paramNames = paramNames; this.handler = handler; } } }